/*
 *    Copyright 2009-2022 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       https://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.executor;

import java.sql.SQLException;
import java.util.List;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.cache.TransactionalCacheManager;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.transaction.Transaction;

/**
 *
 * 二级缓存执行器，设计到二级缓存的相关逻辑，注意：想要使用二级缓存你每次查询时候，要调用提交commit方法
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public class CachingExecutor implements Executor {

  private final Executor delegate;
  /**
   * 事务缓存管理器
   */
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }

  @Override
  public Transaction getTransaction() {
    return delegate.getTransaction();
  }

  @Override
  public void close(boolean forceRollback) {
    try {
      // issues #499, #524 and #573
      if (forceRollback) {
        tcm.rollback();
      } else {
        tcm.commit();
      }
    } finally {
      delegate.close(forceRollback);
    }
  }

  @Override
  public boolean isClosed() {
    return delegate.isClosed();
  }

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }

  @Override
  public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.queryCursor(ms, parameter, rowBounds);
  }

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    /**
     * 获取sql包语句
     * 注意：这里已经的sql是已经处理好的sql语句了，你使用的动态sql，在这就进行处理了,处理结果如下：
     *      select id as id, user_name as userName, password as password
     *           from t_user t
     *            WHERE  id = ?
     */
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    /**
     * 一级和二级的缓存的key组成：
     *      1.com.elite.dao.UserDao.findById（接口的包名和方法名）或者理解是你的Mapper文件的（命名空间和id就是方法名）
     *      2.分页参数： limit  0,2147483647  mybatis的分页参数是 0,2147483647，你不写分页默认就是这个
     *      3.SQL语句: select id ,userName, password from t_user where id = ?
     *      4.sql的查询参数: select id ,userName, password from t_user where id = ?
     *      5.环境id：就是你配置数据源时候的那个环境id： <environment id="development">
     *        <environments default="development">
     *         <environment id="development">
     *             <transactionManager type="JDBC"/>
     *             <dataSource type="POOLED">
     *                 <property name="driver" value="${jdbc.driver}"/>
     *                 <property name="url" value="${jdbc.url}"/>
     *                 <property name="username" value="${jdbc.username}"/>
     *                 <property name="password" value="${jdbc.password}"/>
     *             </dataSource>
     *         </environment>
     *     </environments>
     *
     *  重点要理解：因为涉及到为什么一级缓存失效问题（spring整合mybatis的一级缓存失效），还有事务失效问题也是和会话相关
     *    1. 一级缓存：是会话级别的，spring的一级缓存失效就是会话不同导致的
     *    2. 二级缓存：是应用级别的，可以跨线程使用
     */
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
    //获取该UserMapper.xml声明二级缓存空间。为null说明，没有使用二级缓存
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
          /**
           * 1.如果是第一次进来，那么它会先根据二级缓存创建二级暂存区，最后这个二级暂存区执行真正的二级缓存，如下：
           *    TransactionalCache（这是二级暂存区） -> SynchronizedCache -> LoggingCache -> SerializedCache ->  PerpetualCache 最终从PerpetualCache对象的map数据获取数据
           * 2.注意：
           *    二级缓存是在解析Mapper.xml时候就创建好了，最后放到MappedStatement对象中，这个对象可以理解是在Mapper.xml文件中写的sql语句， 每条sql语句都有对应的MappedStatement对象。
           */
          List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          //交给BaseExecutor执行器查询，该执行器有一级缓存的相关逻辑
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //把从数据库查询的数据放到 暂存区
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    //交给BaseExecutor执行器查询，该执行器有一级缓存的相关逻辑
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  @Override
  public List<BatchResult> flushStatements() throws SQLException {
    return delegate.flushStatements();
  }

  @Override
  public void commit(boolean required) throws SQLException {
    //清除一级缓存
    delegate.commit(required);
    //把二级暂存区数据放到二级缓冲区
    tcm.commit();
  }

  @Override
  public void rollback(boolean required) throws SQLException {
    try {
      delegate.rollback(required);
    } finally {
      if (required) {
        tcm.rollback();
      }
    }
  }

  private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
    if (ms.getStatementType() == StatementType.CALLABLE) {
      for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
        if (parameterMapping.getMode() != ParameterMode.IN) {
          throw new ExecutorException("Caching stored procedures with OUT params is not supported.  Please configure useCache=false in " + ms.getId() + " statement.");
        }
      }
    }
  }

  @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
  }

  @Override
  public boolean isCached(MappedStatement ms, CacheKey key) {
    return delegate.isCached(ms, key);
  }

  @Override
  public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
    delegate.deferLoad(ms, resultObject, property, key, targetType);
  }

  @Override
  public void clearLocalCache() {
    delegate.clearLocalCache();
  }

  private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {
      tcm.clear(cache);
    }
  }

  @Override
  public void setExecutorWrapper(Executor executor) {
    throw new UnsupportedOperationException("This method should not be called");
  }

}
